/*
 * CopyScript.java
 *
 * Created on November 2, 2007, 10:54 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package filecopier.script;

import java.io.File;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.util.List;
import java.util.LinkedList;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Map;
import java.util.TreeMap;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;

import custom.xml.DOMHelperException;

/**
 *
 * @author david
 */
public class CopyScript extends AbstractScript {

    private static final String namespace = "http://sourceforge.net/projects/filecopier01/copy";
    private boolean tempScript = false;

    /** Creates a new instance of CopyScript */
    public CopyScript() throws DOMHelperException {
        super(CopyScript.class.getResource("CopyScript.xsd"), filecopier.Singleton.getInstance().getOptions().getCopyPath());

    }

    public CopyScript(final File scriptFile) throws DOMHelperException, IOException {
        super(CopyScript.class.getResource("CopyScript.xsd"), filecopier.Singleton.getInstance().getOptions().getCopyPath(), scriptFile);
    }

    public CopyScript(final File scriptFile, boolean tempScript) throws DOMHelperException, IOException {
        super(CopyScript.class.getResource("CopyScript.xsd"), filecopier.Singleton.getInstance().getOptions().getCopyPath(), scriptFile);
        this.tempScript = tempScript;
    }

    public boolean addScriptEntry(final ScriptEntry scriptEntry) {
        if (scriptEntry == null) {
            throw new IllegalArgumentException("addScriptEntry() was passed a null for the ScriptEntryInterface.");
        }
        if (!(scriptEntry instanceof CopyScriptEntry)) {
            throw new IllegalArgumentException("addScriptEntry() was passed a ScriptEntryInterface object that was not " +
                    "of type CopyScriptEntry.");
        }
        scriptEntry.setSavedState(ScriptEntry.SavedState.Added);
        // See if this entry is already in the script. If it is, don't add it.
        boolean add = true;
        for (ScriptEntry se : scriptEntries) {
            if (se.equals(scriptEntry)) {
                add = false;
                break;
            }
        }
        if (add) {
            scriptEntries.add(scriptEntry);
        }
        return add;
    }

    public boolean insertScriptEntry(final ScriptEntry scriptEntry, int idx) {
        if (scriptEntry == null) {
            throw new IllegalArgumentException("insertScriptEntry() was passed a null for the ScriptEntryInterface.");
        }
        if (!(scriptEntry instanceof CopyScriptEntry)) {
            throw new IllegalArgumentException("addScriptEntry() was passed a ScriptEntryInterface object that was not " +
                    "of type CopyScriptEntry.");
        }
        scriptEntry.setSavedState(ScriptEntry.SavedState.Added);
        scriptEntries.add(idx, scriptEntry);
        return true;
    }

    public boolean removeScriptEntry(final ScriptEntry scriptEntry) {
        boolean removed = false;
        if (scriptEntry == null) {
            throw new IllegalArgumentException("removeScriptEntry() was passed a null for the ScriptEntry object.");
        }
        if (!(scriptEntry instanceof CopyScriptEntry)) {
            throw new IllegalArgumentException("removeScriptEntry() was passed a ScriptEntry object " +
                    "that was not of type CopyScriptEntry.");
        }
        ScriptEntry scriptToRemove = null;
        // See if this ScriptEntry object is in this RestoreScript object.
        for (ScriptEntry se : scriptEntries) {
            boolean equal = se.equals(scriptEntry);
            if (equal) {
                scriptToRemove = se;
                break;
            }
        }
        // If the ScriptEntry was found, flag it for removal.
        if (scriptToRemove != null) {
            // If the state of this entry is saved, flag it for removal.
            if (scriptToRemove.getSavedState() == ScriptEntry.SavedState.Saved) {
                scriptToRemove.setSavedState(ScriptEntry.SavedState.Removed);
            } // If the state of this entry is added, it has not been added to the file, so remove it from the list.
            else {
                scriptEntries.remove(scriptToRemove);
            }
            removed = true;
        }
        return removed;
    }

    public List<ScriptEntry> getScriptEntries(final ScriptEntry.PathType pathType) {
        if (pathType == null) {
            throw new IllegalArgumentException("getScriptEntries() was passed a null for the PathType.");
        }

        List<ScriptEntry> typeEntries = new LinkedList<ScriptEntry>();
        for (ScriptEntry se : scriptEntries) {
            if (se.getPath(pathType) != null) {
                typeEntries.add(se);
            }
        }

        return typeEntries;
    }

    public List<ScriptEntry> getScriptEntries(final ScriptEntry.SavedState savedState) {
        if (savedState == null) {
            throw new IllegalArgumentException("getScriptEntries() was passed a null for the SavedState.");
        }

        List<ScriptEntry> typeEntries = new LinkedList<ScriptEntry>();
        for (ScriptEntry se : scriptEntries) {
            if (se.getSavedState() == savedState) {
                typeEntries.add(se);
            }
        }

        return typeEntries;
    }

    public boolean isTemp() {
        boolean tempScript = false;

        Node firstChild = domUtilities.getDocument().getFirstChild();
        if (firstChild != null) {
            NamedNodeMap attrs = firstChild.getAttributes();
            if (attrs != null) {
                Node temp = attrs.getNamedItem("Temp");
                tempScript = temp != null && temp.getNodeValue().equals("true");
            }
        }
        return tempScript;
    }

    public void setTemp(boolean temp) {
        tempScript = temp;
        if (temp) {
            Element copyScript = domUtilities.getDocument().getDocumentElement();
            copyScript.setAttribute("Temp", "true");
        }
    }

    protected List<ScriptEntry> initScriptEntries() {
        if (scriptEntries == null) {
            scriptEntries = new LinkedList<ScriptEntry>();
        }
        Node firstChild = domUtilities.getDocument().getFirstChild();
        if (firstChild != null) {
            NodeList nodeList = firstChild.getChildNodes();
            int length = nodeList != null ? nodeList.getLength() : 0;

            for (int i = 0; i < length; i++) {
                Node node = nodeList.item(i);
                if (node.getNodeName().equals("Path")) {
                    ScriptEntry.PathType pathType = getPathTypeFromNode(node);

                    String pathStr = node.getFirstChild().getNodeValue();

                    ScriptEntry scriptEntry = new CopyScriptEntry(pathType,
                            ScriptEntry.SavedState.Saved, new File(pathStr));

                    scriptEntries.add(scriptEntry);
                }
            }
        }
        return scriptEntries;
    }

    protected Document createDocument() throws IOException {
        return (createDocument(false));
    }

    protected Document createDocument(boolean tempScript) throws IOException {
        scriptFile.createNewFile();
        Document doc = domUtilities.getDocument();
        Element copyScript = doc.createElementNS(namespace, "CopyScript");
        copyScript.setAttribute("xmlns", namespace);
        if (tempScript == true) {
            copyScript.setAttribute("Temp", "true");
        }
        doc.appendChild(copyScript);
        return doc;
    }

    protected Element createElement(final ScriptEntry scriptEntry) {
        Document document = domUtilities.getDocument();

        Element path = document.createElement("Path");
        path.setAttribute("Type", scriptEntry.getPathType() == ScriptEntry.PathType.Normal ? "normal" : "script");
        path.appendChild(document.createTextNode(scriptEntry.getPath().toString()));

        Element copyScript = document.getDocumentElement();
        copyScript.appendChild(path);
        return path;
    }

    protected Element insertElement(final ScriptEntry scriptEntry, int idx) {
        Element path = null;
        Document document = domUtilities.getDocument();
        Element copyScript = document.getDocumentElement();
        org.w3c.dom.NodeList nodeList = copyScript.getElementsByTagName("Path");
        int length = nodeList.getLength();
        if (idx < length - 1) {
            path = document.createElement("Path");
            path.setAttribute("Type", scriptEntry.getPathType() == ScriptEntry.PathType.Normal ? "normal" : "script");
            path.appendChild(document.createTextNode(scriptEntry.getPath().toString()));

            org.w3c.dom.Node node = nodeList.item(idx);
            copyScript.insertBefore(path, node);
        } else {
            path = createElement(scriptEntry);
        }

        return path;
    }

    protected boolean removeElement(final ScriptEntry se) {
        boolean removed = false;
        Element copyScript = domUtilities.getDocument().getDocumentElement();
        org.w3c.dom.NodeList nodeList = copyScript.getElementsByTagName("Path");
        for (int i = 0; removed == false && i < nodeList.getLength(); i++) {
            org.w3c.dom.Node path = nodeList.item(i);
            org.w3c.dom.NodeList pathChildren = path.getChildNodes();
            for (int j = 0; j < pathChildren.getLength(); j++) {
                org.w3c.dom.Node pathChild = pathChildren.item(j);
                if (pathChild.getNodeValue().equals(se.getPath().getPath())) {
                    NodeList plc = pathChild.getChildNodes();
                    if (plc != null && plc.getLength() > 0) {
                        for (int k = 0; k < plc.getLength(); k++) {
                            pathChild.removeChild(plc.item(i));
                        }
                    }
                    path.removeChild(pathChild);
                    copyScript.removeChild(path);
                    removed = true;
                    break;
                }
            }
        }
        return removed;
    }

    protected boolean convertOldScript() throws DOMHelperException, IOException {
        boolean converted = false;
        Properties properties = new Properties();
        File oldScriptFile = null;
        File scriptFile = getScriptFile();
        try {
            InputStream is = new FileInputStream(scriptFile);
            properties.load(is);
            is.close();
            if (isOldScript(properties)) {
                // Put the previous version of FileCopier (1.04) between the file name and its extension. This file
                // can be used if the user decides he prefers the old version of FileCopier.
                String oldScriptName = buildOldScriptName(scriptFile.getPath());
                oldScriptFile = new File(oldScriptName);
                File tempScriptFile = scriptFile;
                tempScriptFile.renameTo(oldScriptFile);

                // We just renamed the file, so the file with the original name does not exist anymore. Call init.
                // This time, it should not get a DOMHelperException
                init();
                int addedCt = 0;

                String section = null;
                if (properties.getProperty("file.1") != null) {
                    section = "file.";
                } else if (properties.getProperty("path.1") != null) {
                    section = "path.";
                }

                if (section != null) {
                    // Add all file entries.
                    for (int i = 1;; i++) {
                        String value = properties.getProperty(section + i);
                        if (value != null) {
                            ScriptEntry se = new CopyScriptEntry(ScriptEntry.PathType.Normal, new File(value));
                            addScriptEntry(se);
                            addedCt++;
                        } else {
                            break;
                        }
                    }
                }

                Map<String, String> srchReplaceMap = null;

                // See if there are any script entries.
                for (int i = 1;; i++) {
                    String value = properties.getProperty("script." + i);
                    if (value != null) {
                        addedCt++;
                        ScriptEntry se = new CopyScriptEntry(ScriptEntry.PathType.Script, new File(value));
                        // This is a script file, so initialize it. If it is an old style script, an attempt will
                        // be made to convert it to the new format. If it fails, a DOMHelperException will be thrown.
                        // Catch this exception and rename the old style script back to its original name. Then,
                        // rethrow the exception.
                        try {
                            CopyScript copyScript = new CopyScript(new File(value));
                            // If we get to this point, the inner script was successfully converted. Get the new name
                            // and replace the new name with the old name in the script that called the inner script.
                            if (srchReplaceMap == null) {
                                srchReplaceMap = new TreeMap<String, String>();
                            }

                            // Build the old script name. This is what we will put in the script with the old format.
                            oldScriptName = buildOldScriptName(value);
                            srchReplaceMap.put(value, oldScriptName);
                        } catch (DOMHelperException e) {
                            oldScriptFile.renameTo(scriptFile);
                            throw e;
                        }

                        addScriptEntry(se);
                        addedCt++;
                    } else {
                        break;
                    }
                }

                if (addedCt > 0) {
                    // At least one entry was added to the script file.
                    // See if we need to replace the names of any inner scripts in the old script.
                    if (srchReplaceMap != null) {
                        // This will replace the names of all the inner scripts called in the old script file and
                        // save the file.
                        srchAndReplaceFileNames(srchReplaceMap, oldScriptFile);
                    }

                    // Save the new script file.
                    // First, set the class instance of scriptFile to null, so that this will be treated as the creation of a new file.
                    this.scriptFile = null;
                    saveToDisk(scriptFile);
                    // Make sure what we just created is a valid script file.
                    try {
                        validate();
                    } catch (DOMHelperException e) {
                        // The file is not valid, so preserve the old script file.
                        oldScriptFile.renameTo(scriptFile);
                        throw e;
                    }
                    converted = true;
                    filecopier.Singleton.getInstance().getFileConsoleLogger().info("\"" + scriptFile + "\" was in the old format." +
                            " It was successfully converted to the new format. A copy of the script file in the old format was" +
                            " saved to \"" + oldScriptName + "\"");
                } else {
                    // Rename the old temp file to its original name.
                    oldScriptFile.renameTo(scriptFile);
                    filecopier.Singleton.getInstance().getFileConsoleLogger().warn("\"" + scriptFile + "\" was in the old format." +
                            " It could not be converted to the new format.");
                }
            }
        } catch (IOException e) {
            if (oldScriptFile != null) {
                oldScriptFile.renameTo(scriptFile);
            }
            throw e;
        // If the file is not found, then it is definitely not a script file with the old format. Return false.
        }
        return converted;
    }

    public String buildOldScriptName(final String scriptName) {
        String nameOnly, newFileName, ext, sep;

        int idx = scriptName.lastIndexOf('.');
        if (idx != -1) {
            // File has an extension, so put the name and extension in separate Strings.
            nameOnly = scriptName.substring(0, idx);
            ext = scriptName.substring(idx);
            // Make the separator a dot.
            sep = ".";
        } else {
            /* File has no extension, so set sNameOnly to point to the whole name and set sExt to point to
            an empty String. */
            nameOnly = scriptName;
            ext = "";
            // Make the separator an underscore, so the sytem won't think the added date is an extension.
            sep = "_";
        }

        return (nameOnly + sep + "1" + sep + "04" + ext);
    }

    private void srchAndReplaceFileNames(final Map<String, String> srchReplaceMap, final File oldScript) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(oldScript));
        try {
            StringBuilder src = new StringBuilder();
            StringBuilder dest = new StringBuilder();

            // Get the contents of the script with the old format and put it in a string.
            String line = null;
            while ((line = br.readLine()) != null) {
                src.append(line + System.getProperty("line.separator"));
            }

            String newContents = src.toString();
            int changeCt = 0;

            for (Map.Entry<String, String> mapEntry : srchReplaceMap.entrySet()) {
                String srchStr = mapEntry.getKey();
                String replaceStr = mapEntry.getValue();
                String result = srchAndReplace(newContents, srchStr, replaceStr);
                if (result != null) {
                    changeCt++;
                    newContents = result;
                }
            }

            // If changeCt is greater than 0, we replaced at least one inner script name. Save it to the file.
            if (changeCt > 0) {
                BufferedWriter bw = new BufferedWriter(new FileWriter(oldScript));
                try {
                    bw.write(newContents);
                } finally {
                    bw.close();
                }
            }
        } finally {
            br.close();
        }
    }

    private String srchAndReplace(final String subject, final String srchStr, final String replaceStr) throws IOException {
        StringBuilder dest = new StringBuilder();

        Pattern srchPattern = Pattern.compile(srchStr);
        Matcher srchMatcher = srchPattern.matcher(subject);
        int matchCt = 0;
        int startIdx = 0;

        while (srchMatcher.find()) {
            matchCt++;
            int matchStart = srchMatcher.start();
            int matchEnd = srchMatcher.end();
            dest.append(subject.substring(startIdx, matchStart));
            dest.append(replaceStr);
            startIdx = matchEnd;
        }

        // Copy anything that remains in the original string.
        dest.append(subject.substring(startIdx));

        return (matchCt > 0 ? dest.toString() : null);
    }

    private boolean isOldScript(Properties properties) {
        // If this file is an old from script file, it will have a property with a key value of "file.1". If this file is
        // an old to script file, it will have a property with a key value of "path.1". If it does not have either of these,
        // it could still be a valid script file with the old format if it has a property with the key value of "script.1".
        return ((properties.getProperty("file.1") != null || properties.getProperty("path.1") != null ||
                properties.getProperty("script.1") != null));
    }

    private ScriptEntry.PathType getPathTypeFromNode(final Node node) {
        Node type = node.getAttributes().getNamedItem("Type");
        String pathTypeStr = type.getNodeValue();

        return (pathTypeStr.equals("normal") ? ScriptEntry.PathType.Normal : ScriptEntry.PathType.Script);
    }
}

